#include "WindowsAPI.h"

#include "Core/GlobalContext.h"

#include "Events/MouseEvent.h"
#include "Events/WindowEvent.h"
#include "Events/KeyboardEvent.h"

#include <GLFW/glfw3.h>

namespace SE
{
    Ref<WindowContext> WindowAPI::CreateContext()
    {
        SE_PROFILE_FUNCTION();

        auto newContext = CreateRef<WindowContext>();
        newContext->running = true;

        GLFWwindow* nativeWindow = nullptr;
        {
            SE_PROFILE_SCOPE("glfwCreateWindow");

            nativeWindow = glfwCreateWindow(
                I32(newContext->width),
                I32(newContext->height),
                newContext->title.data(),
                nullptr,
                nullptr);
            glfwMakeContextCurrent(nativeWindow);
            glfwSetWindowUserPointer(nativeWindow, newContext.get());

            newContext->nativeWindow = nativeWindow;
        }

        if (newContext->vsync)
            glfwSwapInterval(1);
        else
            glfwSwapInterval(0);

        // Set GLFW callbacks
        glfwSetWindowSizeCallback(nativeWindow, [](GLFWwindow* window, int width, int height)
        {
            auto& data = *(WindowContext*)glfwGetWindowUserPointer(window);
            data.width = width;
            data.height = height;

            WindowResizeEvent event(width, height);
            data.eventCallback(event);
        });

        glfwSetWindowCloseCallback(nativeWindow, [](GLFWwindow* window)
        {
            auto& data = *(WindowContext*)glfwGetWindowUserPointer(window);

            WindowShouldCloseEvent event;
            data.eventCallback(event);
        });

        glfwSetKeyCallback(nativeWindow, [](GLFWwindow* window, int key, int scancode, int action, int mods)
        {
            auto& data = *(WindowContext*)glfwGetWindowUserPointer(window);

            switch (action)
            {
                case GLFW_PRESS:
                {
                    KeyboardEvent event(KeyboardButton(key), ButtonState::Press);
                    data.eventCallback(event);
                    break;
                }
                case GLFW_RELEASE:
                {
                    KeyboardEvent event(KeyboardButton(key), ButtonState::Release);
                    data.eventCallback(event);
                    break;
                }
                case GLFW_REPEAT:
                {
                    KeyboardEvent event(KeyboardButton(key), ButtonState::Repeat);
                    data.eventCallback(event);
                    break;
                }
            }
        });

        glfwSetCharCallback(nativeWindow, [](GLFWwindow* window, unsigned int keycode)
        {
            auto& data = *(WindowContext*)glfwGetWindowUserPointer(window);

            KeyboardEvent event(KeyboardButton(keycode), ButtonState::Type);
            data.eventCallback(event);
        });

        glfwSetMouseButtonCallback(nativeWindow, [](GLFWwindow* window, int button, int action, int mods)
        {
            auto& data = *(WindowContext*)glfwGetWindowUserPointer(window);

            switch (action)
            {
                case GLFW_PRESS:
                {
                    MouseButtonEvent event(MouseButton(button), ButtonState::Press);
                    data.eventCallback(event);
                    break;
                }
                case GLFW_RELEASE:
                {
                    MouseButtonEvent event(MouseButton(button), ButtonState::Release);
                    data.eventCallback(event);
                    break;
                }
            }
        });

        glfwSetScrollCallback(nativeWindow, [](GLFWwindow* window, double xOffset, double yOffset)
        {
            auto& data = *(WindowContext*)glfwGetWindowUserPointer(window);

            MouseScrollEvent event(xOffset, yOffset);
            data.eventCallback(event);
        });

        glfwSetCursorPosCallback(nativeWindow, [](GLFWwindow* window, double xPos, double yPos)
        {
            auto& data = *(WindowContext*)glfwGetWindowUserPointer(window);

            MouseMoveEvent event(xPos, yPos);
            data.eventCallback(event);
        });

        return newContext;
    }

    void WindowAPI::DestoryContext(const Ref<WindowContext>& context)
    {
        SE_PROFILE_FUNCTION();

        SE_ASSERT(context, "Current context is already expired!");

        glfwDestroyWindow((GLFWwindow*)(context->nativeWindow));
    }

    inline void GLFWErrorCallback(int error, const char* description)
    {
        LOG_ERROR("GLFW Error ({0}): {1}", error, description);
    }

	WindowsAPI::WindowsAPI()
	{
        SE_PROFILE_FUNCTION();

        {
            SE_PROFILE_SCOPE("glfwInit");
            SE_ASSERT(glfwInit(), "Could not initialize GLFW!");
            glfwWindowHint(GLFW_CONTEXT_VERSION_MAJOR, 4);
            glfwWindowHint(GLFW_CONTEXT_VERSION_MINOR, 5);
            glfwWindowHint(GLFW_OPENGL_PROFILE, GLFW_OPENGL_CORE_PROFILE);
        }

        glfwSetErrorCallback(GLFWErrorCallback);
	}

    WindowsAPI::~WindowsAPI()
    {
        SE_PROFILE_FUNCTION();

        glfwTerminate();
    }

	WindowAPIType WindowsAPI::getAPIType() const
	{
		return WindowAPIType::GLFW;
	}

    void WindowsAPI::makeContextCurrent(const Ref<WindowContext>& context)
    {
        SE_PROFILE_FUNCTION();

        SE_ASSERT(context, "Empty context!");

        mCurrentContext = context;
        glfwMakeContextCurrent((GLFWwindow*)(mCurrentContext.lock()->nativeWindow));
    }

    void WindowsAPI::setTitle(const String& title)
    {
        SE_PROFILE_FUNCTION();

        SE_ASSERT(!mCurrentContext.expired(), "Current context is already expired!");

        glfwSetWindowTitle((GLFWwindow*)(mCurrentContext.lock()->nativeWindow), title.c_str());
    }

    void WindowsAPI::setVSync(const Boolean vsync)
    {
        SE_PROFILE_FUNCTION();

        SE_ASSERT(!mCurrentContext.expired(), "Current context is already expired!");

        if (vsync)
            glfwSwapInterval(1);
        else
            glfwSwapInterval(0);

        mCurrentContext.lock()->vsync = vsync;
    }

    void WindowsAPI::setCursorFocused(const Boolean focused)
    {
        SE_PROFILE_FUNCTION();

        SE_ASSERT(!mCurrentContext.expired(), "Current context is already expired!");

        glfwSetInputMode((GLFWwindow*)(mCurrentContext.lock()->nativeWindow), GLFW_CURSOR, focused ? GLFW_CURSOR_DISABLED : GLFW_CURSOR_NORMAL);
    }

    void WindowsAPI::pollEvents()
    {
        SE_PROFILE_FUNCTION();

        glfwPollEvents();
    }

    void WindowsAPI::swapBuffers()
    {
        SE_PROFILE_FUNCTION();

        SE_ASSERT(!mCurrentContext.expired(), "Current context is already expired!");

        glfwSwapBuffers((GLFWwindow*)(mCurrentContext.lock()->nativeWindow));
    }

    Vector2i WindowsAPI::getWindowSize() const
    {
        SE_PROFILE_FUNCTION();

        SE_ASSERT(!mCurrentContext.expired(), "Current context is already expired!");

        I32 width, height;
        glfwGetWindowSize((GLFWwindow*)(mCurrentContext.lock()->nativeWindow), &width, &height);

        return { width, height };
    }

    Vector2f WindowsAPI::getMousePosition() const
    {
        SE_PROFILE_FUNCTION();

        SE_ASSERT(!mCurrentContext.expired(), "Current context is already expired!");

        Double xpos, ypos;
        glfwGetCursorPos((GLFWwindow*)(mCurrentContext.lock()->nativeWindow), &xpos, &ypos);

        return { (Float)xpos, (Float)ypos };
    }

    Boolean WindowsAPI::isMouseButtonPressed(const MouseButton button) const
    {
        SE_PROFILE_FUNCTION();

        SE_ASSERT(!mCurrentContext.expired(), "Current context is already expired!");

        return GLFW_PRESS == glfwGetKey((GLFWwindow*)(mCurrentContext.lock()->nativeWindow), static_cast<I32>(button));
    }

    Boolean WindowsAPI::isKeyboardButtonPressed(const KeyboardButton button) const
    {
        SE_PROFILE_FUNCTION();

        SE_ASSERT(!mCurrentContext.expired(), "Current context is already expired!");

        return GLFW_PRESS == glfwGetKey((GLFWwindow*)(mCurrentContext.lock()->nativeWindow), static_cast<I32>(button));
    }
} // !namespace SE